package de.bk.timecalc.core;

import java.util.Random;

/**
 * Core logic of TimeCalc.
 * 
 * Test: {@link TimeCalcCoreTest}.
 * 
 * @author Bastian Krol
 * @version $Revision:$ $Date:$ $Author:$
 */
public class TimeCalcCore
{
  private static final Random RANDOM = new Random();
  private static final int MAX_SECONDS = 20 * 60;

  private boolean timerCountsDown;

  private ChallengeDTO lastChallenge;
  private TimeDTO correctAnswer;
  private Duration timeNeededForAnswer;

  private int challengesDone;
  private float scoreForLastAnswer;
  private float score;

  private IScoringSchema scoringSchema;
  private StopWatch stopWatch;

  /**
   * Creates a new TimeCalcCore object.
   */
  public TimeCalcCore()
  {
    this.timerCountsDown = true;
    this.scoringSchema = new MidletScoringSchema();
    this.stopWatch = new StopWatch();
  }

  /**
   * Starts the next time calc challenge.
   */
  public ChallengeDTO nextChallenge()
  {
    Difference difference = this.selectRandomDifference();
    TimeDTO time = this.createRandomTime(difference);
    this.lastChallenge = new ChallengeDTO(time, difference);
    this.correctAnswer = this.calculateAnswer(this.lastChallenge);
    this.stopWatch.start();
    return this.lastChallenge;
  }

  private Difference selectRandomDifference()
  {
    // non uniform distribution: select 35 seconds and 25 seconds twice as much
    // as 2 minutes
    if (Difference.ALL_DIFFERENCES.length != 3)
    {
      throw new RuntimeException(
          "Expected 3 possible differences, but that seems to have changed. Code in TimeCalcCore needs to be adapted.");
    }
    Difference[] differences = new Difference[] {
        Difference.TWENTY_FIVE_SECONDS, Difference.TWENTY_FIVE_SECONDS,
        Difference.THIRTY_FIVE_SECONDS, Difference.THIRTY_FIVE_SECONDS,
        Difference.TWO_MINUTES };
    int differenceOrdinal = Math.abs(RANDOM.nextInt()) % differences.length;
    return differences[differenceOrdinal];

    // For a unifom distributen we could just do:
    // int differenceOrdinal =
    // Math.abs(RANDOM.nextInt()) % Difference.ALL_DIFFERENCES.length;
    // return Difference.ALL_DIFFERENCES[differenceOrdinal];
  }

  private TimeDTO createRandomTime(Difference difference)
  {
    int randomSeconds;
    if (this.timerCountsDown)
    {
      int minSeconds = difference.getSeconds();
      // generate a random number between minSeconds and MAX_SECONDS
      randomSeconds = (Math.abs(RANDOM.nextInt()) % (MAX_SECONDS - minSeconds))
          + minSeconds;
    }
    else
    {
      // generate a random number between 0 and max seconds
      randomSeconds = Math.abs(RANDOM.nextInt()) % MAX_SECONDS
          - difference.getSeconds();
    }

    int minutes = randomSeconds / 60;
    int secondsInMinute = randomSeconds % 60;
    return new TimeDTO(minutes, secondsInMinute);
  }

  /**
   * Check the answers for the last challenge for correctness
   * 
   * @param answer
   * @return <code>true</code> if the answer was right, else <code>false</code>
   */
  public boolean checkAnswerForLastChallenge(TimeDTO answer)
  {
    if (this.correctAnswer == null)
    {
      throw new IllegalStateException(
          "nextChallenge() needs to be called at least once before checkAnswerForLastChallenge(TimeDTO) is called.");
    }
    if (answer == null)
    {
      throw new IllegalArgumentException("answer may not be null.");
    }
    this.timeNeededForAnswer = this.stopWatch.stop();
    this.scoreForLastAnswer = this.scoringSchema.calculateScore(
        this.timerCountsDown, this.correctAnswer, answer,
        this.timeNeededForAnswer);
    this.score += this.scoreForLastAnswer;
    this.challengesDone++;
    return answer.equals(this.correctAnswer);
  }

  private TimeDTO calculateAnswer(ChallengeDTO challenge)
  {
    TimeDTO time = challenge.getTime();
    int secondsChallenge = time.getMinute() * 60 + time.getSecond();
    int secondsAnswer;
    if (this.timerCountsDown)
    {
      secondsAnswer = secondsChallenge - challenge.getDifference().getSeconds();
    }
    else
    {
      secondsAnswer = secondsChallenge + challenge.getDifference().getSeconds();
    }
    int minuteAnswer = secondsAnswer / 60;
    int secondsInMinuteAnswer = secondsAnswer % 60;
    return new TimeDTO(minuteAnswer, secondsInMinuteAnswer);
  }

  public TimeDTO getAnswerForLastChallenge()
  {
    return this.correctAnswer;
  }

  /**
   * Returns the duration between the last call to nextChallenge() and the last
   * call checkAnswerForLastChallenge(TimeDTO). If nextChallenge() is called
   * after checkAnswerForLastChallenge(TimeDTO) has been called and
   * checkAnswerForLastChallenge(TimeDTO) has not yet been called again, these
   * calls to nextChallenge() have no effect on the Duration returned by this
   * method.
   * 
   * @return the duration between the last call to nextChallenge() and the last
   *         call checkAnswerForLastChallenge(TimeDTO)
   */
  public Duration getTimeNeededForLastAnswer()
  {
    if (this.timeNeededForAnswer == null)
    {
      throw new IllegalStateException(
          "nextChallenge() and checkAnswerForLastChallenge(TimeDTO) need to be called at least once before getTimeNeededForAnswer() is called.");
    }
    return this.timeNeededForAnswer;
  }

  public final float getScoreForLastAnswer()
  {
    return this.scoreForLastAnswer;
  }

  public final float getScore()
  {
    return this.score;
  }

  public final int getChallengesDone()
  {
    return this.challengesDone;
  }  

  public void setTimerCountsDown(boolean timerCountsDown)
  {
    this.timerCountsDown = timerCountsDown;
  }

  public boolean isTimerCountsDown()
  {
    return this.timerCountsDown;
  }

  public void cancelChallenge()
  {
    this.lastChallenge = null;
    this.correctAnswer = null;
    this.stopWatch.stop();
  }

  public void reset()
  {
    this.timeNeededForAnswer = null;
    this.challengesDone = 0;
    this.scoreForLastAnswer = 0f;
    this.score = 0f;
  }
}
